package cn.mapland.yuanlu.updater.bukkit;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.logging.Level;

import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

import cn.mapland.yuanlu.updater.Updater;
import lombok.Getter;
import lombok.NonNull;
import lombok.val;

/**
 * Bukkit更新器
 * 
 * @author yuanlu
 * @see <a href= "https://git.mapland.cn/yuanlu/UpdaterClient-bukkit">GitLu</a>
 * @see <a href=
 *      "http://dx.mapland.cn:48089/#browse/browse:mapland:cn%2Fmapland%2Fyuanlu%2FUpdaterClient-bukkit">maven</a>
 */
public class BukkitUpdater extends Updater {
	@SuppressWarnings("javadoc")
	private static Conf buildConf(JavaPlugin p0, Conf conf0) {
		if (p0 == null) throw new NullPointerException("JavaPlugin cannot be null.");
		return touchPluginConf(p0, conf0 == null ? new Conf() : conf0, (p, conf) -> {
			PluginDescriptionFile desc = p.getDescription();
			if (conf.getType() == null) conf.setType("mc-bukkit");
			if (conf.getName() == null) conf.setName(p.getName());
			if (conf.getNowVersion() == null) conf.setNowVersion(desc.getVersion());
			if (conf.getTmpDir() == null) conf.setTmpDir(Bukkit.getUpdateFolderFile());
		});
	}

	/**
	 * 使用插件配置<br>
	 * 将会尝试读取指定配置文件, 若不存在将会使用默认配置文件<br>
	 * 修正配置文件后将会保存
	 * 
	 * @param plugin  插件
	 * @param def     插件给出的默认配置
	 * @param builder 配置修正
	 * @return 配置文件
	 * 
	 */
	private static Conf touchPluginConf(JavaPlugin plugin, Conf def, BiConsumer<JavaPlugin, Conf> builder) {
		plugin.getDataFolder().mkdirs();
		File	file	= new File(plugin.getDataFolder(), "y-updater.json");
		if (file.exists()) {
			plugin.getLogger().info(() -> {
				val	r	= file.canRead() ? 'r' : '-';
				val	w	= file.canWrite() ? 'w' : '-';
				val	x	= file.canExecute() ? 'x' : '-';
				return "[DEBUG] 文件: " + file + ", rwx: " + r + w + x;
			});
			try (val in = new FileInputStream(file)) {
				Conf x = JSON.parseObject(in, Conf.class);
				if (x == null) throw new NullPointerException("Null conf" + (file.length() > 0 ? "" : ": Empty File"));
				else def = x;
			} catch (Exception e) {
				plugin.getLogger().log(Level.WARNING, e, () -> String.format("无法读取插件 %s 的更新配置文件: %s", plugin.getName(), file));
				def = def.clone();
				file.renameTo(new File(file.getParentFile(), file.getName() + "." + System.currentTimeMillis() + ".bak"));
			}
		} else def = def.clone();

		try {
			builder.accept(plugin, def);
		} catch (Throwable e) {
			plugin.getLogger().log(Level.SEVERE, e, () -> String.format("无法初始化插件 %s 的更新配置: ", plugin.getName()));
		}

		try (FileWriter writer = new FileWriter(file, false)) {
//			gson.toJson(def, writer);
			JSON.writeJSONString(writer, def, SerializerFeature.PrettyFormat);
		} catch (Exception e) {
			plugin.getLogger().log(Level.WARNING, e, () -> String.format("无法保存插件 %s 的更新配置文件: %s", plugin.getName(), file));
		}
		return def;
	}

	/**
	 * 尝试全自动下载
	 * 
	 * @param plugin 插件
	 * @return 是否成功
	 */
	public static boolean tryUpdate(JavaPlugin plugin) {
		return new BukkitUpdater(plugin, true).auto();
	}

	/** java plugin */
	private final JavaPlugin		plugin;
	/** 版本信息 */
	private @Getter VersionInfo		versions;
	/** 选定版本 */
	private @Getter SimpleVersion	version;

	/** 版本的资源文件 */
	private @Getter Assets			assets;

	/** 更新命令 */
	private Command					cmd;

	/** @param plugin 插件 */
	public BukkitUpdater(@NonNull JavaPlugin plugin) {
		this(plugin, null);
	}

	/**
	 * 静态调用: auto
	 * 
	 * @param plugin 插件
	 * @param auto   auto
	 */
	private BukkitUpdater(@NonNull JavaPlugin plugin, boolean auto) {
		super(buildConf(plugin, null));
		this.plugin = plugin;
		load();
	}

	/**
	 * @param plugin 插件
	 * @param conf   配置文件
	 */
	public BukkitUpdater(@NonNull JavaPlugin plugin, Conf conf) {
		super(buildConf(plugin, conf));
		this.plugin = plugin;
		Bukkit.getScheduler().runTaskAsynchronously(plugin, this::load);
		if (super.conf.isAutoUpdate()) Bukkit.getScheduler().runTaskAsynchronously(plugin, this::auto);
	}

	/**
	 * 全自动下载
	 * 
	 * @return 是否成功
	 */
	public boolean auto() {
		try {
			return update(null, x -> x.iterator().next()) != null;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}

	/** 启用每日检查 */
	public void dailyCheck() {
		notice(Bukkit.getConsoleSender(), true);
		Bukkit.getScheduler().runTaskTimer(plugin, () -> {
			Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
				load();
				notice(Bukkit.getConsoleSender(), true);
			});
		}, 1000 * 60 * 60 * 24, 1000 * 60 * 60 * 24);
	}

	/**
	 * 下载资源
	 * 
	 * @param name 要下载的资源名
	 * @return 下载的文件
	 * @throws IOException IOE
	 */
	private File download(@NonNull String name) throws IOException {
//		return download(url, getPluginFile().getName());
		if (assets == null) throw new NullPointerException("assets not load!");
		if (assets.getData() == null) return null;
		val url = assets.getData().get(name);
		return super.download(url, name);
	}

	/** @return 插件jar文件 */
	File getPluginFile() {
		if (this.plugin == null) return null;
		try {
			Method method = JavaPlugin.class.getDeclaredMethod("getFile");
			method.setAccessible(true);
			return (File) method.invoke(this.plugin);
		} catch (ReflectiveOperationException e) {
			throw new RuntimeException("Could not get plugin file", e);
		}
	}

	/** 加载资源数据 */
	private synchronized void load() {
		sendMsg(Bukkit.getConsoleSender(), "开始检查更新: " + plugin.getDescription().getFullName());
		try {
			this.versions	= getNewVersion();
			this.version	= versions.select(conf);
			BukkitUpdater.this.notifyAll();
		} catch (IOException e) {
			plugin.getLogger().warning("无法获取更新: " + e);
			e.printStackTrace();
		}
	}

	/**
	 * 加载资源文件
	 * 
	 * @param sender 用户
	 * @param pre    是否使用预览版(true 强制预览版, false 强制稳定版, null 保持默认)
	 * @throws IOException IO错误
	 * @see #update(Boolean, Function)
	 * @see #update(CommandSender, Boolean, String)
	 */
	private synchronized void loadAssets(CommandSender sender, Boolean pre) throws IOException {
		if (versions == null) try {
			BukkitUpdater.this.wait(1000 * 60);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if (versions == null) {
			sendMsg(sender, "§c查询新版本失败...");
			return;
		}
		Updater.SimpleVersion v;
		if (pre != null) v = pre ? versions.getPrerelease() : versions.getNormal();
		else v = versions.select(conf);
		boolean change = version != v;
		version = v;
		if (v == null) {
			sendMsg(sender, "插件 §6" + plugin.getName() + "§a 暂无更新.");
			assets = null;
			return;
		}
		if (change || assets == null) assets = getAssets(v);
	}

	/**
	 * 不通知用户<br>
	 * 可重写
	 * 
	 * @param sender 目标
	 * @return 是否不通知
	 */
	protected boolean noNotice(@NonNull CommandSender sender) {
		return !sender.isOp();
	}

	/**
	 * 通知用户有更新<br>
	 * {@link #noNotice(CommandSender) 非强制下只通知管理员}
	 * 
	 * @param sender 用户
	 * @param force  是否强制
	 * 
	 */
	public void notice(@NonNull CommandSender sender, boolean force) {
		if (!force && noNotice(sender)) return;
		Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
			if (versions == null) synchronized (this) {
				try {
					this.wait(1000 * 60);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			if (versions == null || !versions.hasNew()) return;

			String v_nor = null, v_pre = null;
			if (versions.getNormal() != null) v_nor = versions.getNormal().info() + (version == versions.getNormal() ? "§e(选定)" : "");
			if (versions.getPrerelease() != null) v_pre = versions.getPrerelease().info() + (version == versions.getPrerelease() ? "§e(选定)" : "");
			sendMsg(sender, //
					"========================", //
					"插件: §6" + plugin.getName() + "§a 有更新!", //
					"当前版本: §b" + plugin.getDescription().getVersion(), //
					v_nor == null ? null : ("最新稳定版: §b" + v_nor), //
					v_pre == null ? null : ("最新预览版: §b" + v_pre), //
					cmd == null ? null : ("使用命令: §6/" + cmd.getName() + "§a 更新插件"), //
					"========================"//
			);

		});
	}

	/** 不强制的通知每一个进入服务器的人 */
	public void noticeJoin() {
		Bukkit.getServer().getPluginManager().registerEvents(new Listener() {
			@EventHandler
			public void onJoin(PlayerJoinEvent e) {
				notice(e.getPlayer(), false);
			}
		}, plugin);
	}

	/**
	 * 发送消息
	 * 
	 * @param sender 用户
	 * @param str    内容
	 */
	private void sendMsg(CommandSender sender, String... str) {
		if (sender == null) return;
		int			j		= 0;
		String[]	strs	= new String[str.length];
		for (int i = 0; i < str.length; i++) if (str[i] != null) strs[j++] = "§b[自动更新]§a" + str[i];
		str = new String[j];
		System.arraycopy(strs, 0, str, 0, j);
		sender.sendMessage(str);
	}

	/**
	 * 更新插件<br>
	 * 面向code
	 * 
	 * @param pre      是否使用预览版(true 强制预览版, false 强制稳定版, null 保持默认)
	 * @param selector 选择器, 当有多个资源文件时使用
	 * @return 下载的文件
	 * @throws IOException IO错误
	 */
	public File update(Boolean pre, Function<Set<String>, String> selector) throws IOException {
		loadAssets(null, pre);
		if (assets == null || assets.getData() == null) return null;
		int amount = assets.getData().size();
		if (amount == 0) return null;
		String select;
		if (amount > 1) select = selector == null ? null : selector.apply(assets.getData().keySet());
		else select = assets.getData().firstKey();
		return download(assets.getData().get(select));
	}

	/**
	 * 更新插件<br>
	 * 面向cmd
	 * 
	 * @param sender 用户
	 * @param pre    是否使用预览版(true 强制预览版, false 强制稳定版, null 保持默认)
	 * @param select 选择的文件
	 * @return 下载的文件
	 */
	private File update(CommandSender sender, Boolean pre, String select) {
		try {
			loadAssets(sender, pre);
			if (assets == null) return null;
		} catch (IOException e) {
			e.printStackTrace();
			sendMsg(sender, "§c检查时发生IO错误", "§c" + e);
			return null;
		}
		int		amount	= assets.getData() != null ? assets.getData().size() : 0;
		String	url;
		if (amount == 0) {
			sendMsg(sender, "未检测到对应的资源文件, 请手动更新!");
			return null;
		} else if (amount > 1 && select == null) {
			ArrayList<String> msgs = new ArrayList<>(amount + 2);
			msgs.add("检测到多个匹配的资源文件");
			msgs.add("使用命令: §6/" + cmd.getName() + "use <file>§a 更新插件:");
			assets.getData().keySet().forEach(msgs::add);
			sendMsg(sender, msgs.toArray(new String[msgs.size()]));
			return null;
		} else if (amount == 1) {
			val e = assets.getData().firstEntry();
			url		= e.getValue();
			select	= e.getKey();
		} else {
			url = assets.getData().get(select);
			if (url == null) {
				sendMsg(sender, "§c找不到指定的资源文件: " + select);
				return null;
			}
		}
		val full = plugin.getDescription().getFullName();
		sendMsg(sender, "准备开始更新", "当前插件: §6" + full, "新文件: §6" + select, "地址: §6" + url);
		try {
			File f = download(select);
			if (f == null) sendMsg(sender, "下载 §6" + full + " §a时出现未知错误. 更新失败!");
			else sendMsg(sender, "已下载好更新文件: §6" + f.getPath() + "§a for §e" + full);
			return f;
		} catch (IOException e) {
			e.printStackTrace();
			sendMsg(sender, "§c下载时发生IO错误", "§c" + e);
		}
		return null;
	}

	/**
	 * 使用命令<br>
	 * 命令名称默认: update-插件名<br>
	 * 命令权限默认: update.插件名
	 * 
	 * @return 是否成功注册
	 */
	public boolean useCmd() {
		return useCmd(null, null);
	}

	/**
	 * 使用命令<br>
	 * 命令名称默认: update-插件名<br>
	 * 命令权限默认: update.插件名
	 * 
	 * @param cmdName       命令名称, 传入null将使用默认值
	 * @param cmdPermission 命令权限, 传入null将使用默认值
	 * @return 是否成功注册
	 */
	public boolean useCmd(String cmdName, String cmdPermission) {
		final String	name		= cmdName == null ? "update-" + plugin.getName() : cmdName;
		final String	permission	= cmdPermission == null ? "update." + plugin.getName() : cmdPermission;
		final Command	cmd			=																									//
				new Command(name) {
					final List<String> TAB = Arrays.asList("normal", "prerelease", "default", "use");

					@Override
					public boolean execute(CommandSender sender, String commandLabel, String[] args) {
						if (!testPermission(sender)) return true;
						Boolean selectPrerelease = null;
						String selectAssets	= null;
						if (args.length > 0) {
							String arg = args[0].toLowerCase();
							if (arg.startsWith("n")) selectPrerelease = false;
							else if (arg.startsWith("p")) selectPrerelease = true;
							else if (arg.startsWith("d")) selectPrerelease = null;
							else if (arg.startsWith("u")) {
								if (args.length > 1) selectAssets = args[1];
								else {
									sendMsg(sender, "使用命令: §6/" + BukkitUpdater.this.cmd.getName() + "use <file>§a 指定更新文件");
									return true;
								}
							} else return help(sender);
							final Boolean _selectPrerelease = selectPrerelease;
							final String _selectAssets	= selectAssets;
							Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> update(sender, _selectPrerelease, _selectAssets));
							return true;
						} else return help(sender);
					}

					private List<String> getList(String prefix, Collection<String> c) {
						prefix = prefix.toLowerCase();
						ArrayList<String> list = new ArrayList<>(c.size());
						for (String str : c) if (str.toLowerCase().startsWith(prefix)) list.add(str);
						return list;
					}

					private boolean help(CommandSender sender) {
						val cmd = "/" + BukkitUpdater.this.cmd.getName();
						sendMsg(sender,																									//
								cmd + " <nor/pre/def> §6更新稳定/预览/默认版本",																	//
								cmd + " use <file> §6指定更新文件"																			//
						);
						return true;
					}

					@Override
					public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
						if (args.length == 1) return getList(args[0], TAB);
						else if (args.length == 2) return getList(args[1], assets.getData().keySet());
						else return Collections.emptyList();
					}
				};
		cmd.setPermission(permission);
		try {
			Method		method	= Bukkit.getServer().getClass().getMethod("getCommandMap");
			CommandMap	cmdm	= (CommandMap) method.invoke(Bukkit.getServer());
			boolean		b		= cmdm.register(plugin.getName(), cmd);
			if (b) this.cmd = cmd;
			return b;
		} catch (Exception e2) {
			System.err.println("CAN NOT REGISTER COMMAND: " + e2.toString());
			plugin.getLogger().warning("CAN NOT REGISTER COMMAND: " + e2.toString());
			return false;
		}
	}
}
